package com.patternity.dotgraph;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

/**
 * A simple API to generate Dot (Graphviz) files from a tree of Node and
 * Associations. The output is the content of a dot file.
 * 
 * @author cyrille martraire
 */
@SuppressWarnings("unchecked")
public final class DotGraph implements Renderable {

	private final static String getString(final String key) {
		return DotStyles.getString(key);
	}

	public static final String HIDE_OPTIONS = null;

	public static final String CLASS_NODE_OPTIONS = getString("CLASS_NODE_OPTIONS");

	public static final String NOTE_NODE_OPTIONS = getString("NOTE_NODE_OPTIONS");

	public static final String STUB_NODE_OPTIONS = getString("STUB_NODE_OPTIONS");

	public static final String COLLABORATION_NODE_OPTIONS = getString("COLLABORATION_NODE_OPTIONS");

	public static final String ELLIPSIS_NODE_OPTIONS = getString("ELLIPSIS_NODE_OPTIONS");

	// ---

	public static final String ASSOCIATION_EDGE_STYLE = getString("ASSOCIATION_EDGE_STYLE");

	public static final String INSTANTIATION_EDGE_STYLE = getString("INSTANTIATION_EDGE_STYLE");

	public static final String IMPLEMENTS_EDGE_STYLE = getString("IMPLEMENTS_EDGE_STYLE");

	public static final String EXTENDS_EDGE_STYLE = getString("EXTENDS_EDGE_STYLE");

	public static final String NOTE_EDGE_STYLE = getString("NOTE_EDGE_STYLE");

	public static final String COLLABORATION_EDGE_STYLE = getString("COLLABORATION_EDGE_STYLE");

	public static final String CLIENT_EDGE_STYLE = getString("CLIENT_EDGE_STYLE");

	// -----

	private static final String CLUSTER_PREFIX = "\n"
			+ getString("CLUSTER_PREFIX");

	private final AbstractNode root;

	private final NodeRegistry registry = new NodeRegistry();

	public DotGraph(final String title) {
		root = new Digraph(registry, title);
	}

	public Digraph getDigraph() {
		return (Digraph) root;
	}

	public String preloadNode(final Object id) {
		return registry.nodeUniqueId(id);
	}

	@Override
	public String render() {
		return root.render();
	}

	@Override
	public String toString() {
		return "DotGraph root: " + root;
	}

	/**
	 * Stores unique node id by String node key
	 * 
	 * @author cyrille martraire
	 */
	public static final class NodeRegistry {
		private final Map nodeUids = new HashMap();

		private int count = 0;

		public String existingUniqueId(final Object id) {
			final String nodeUid = (String) nodeUids.get(id);
			return nodeUid;
		}

		public String nodeUniqueId(final Object id) {
			String nodeUid = (String) nodeUids.get(id);
			if (nodeUid != null) {
				return nodeUid;
			}
			nodeUid = "c" + count++;
			nodeUids.put(id, nodeUid);
			return nodeUid;
		}

		@Override
		public String toString() {
			return "NodeRegistry: " + nodeUids.size() + " nodes registered";
		}
	}

	/**
	 * Represents any abstract node (digraph, cluster, node, record cell)
	 * 
	 * @author cyrille martraire
	 */
	public static abstract class AbstractNode implements Renderable {

		protected final NodeRegistry registry;

		protected final String id;

		protected String comment;

		protected String label;

		protected String options;

		protected final Collection stereotypes = new HashSet();

		protected final Map nodes = new HashMap();

		protected final Collection associations = new HashSet();

		public AbstractNode(final NodeRegistry registry, final String id) {
			this.registry = registry;
			this.id = id;
		}

		public String getComment() {
			return comment;
		}

		public AbstractNode setComment(final String comment) {
			this.comment = comment;
			return this;
		}

		public Node addPossibleNode(final Object id) {
			final String uid = registry.existingUniqueId(id);
			if (uid == null) {
				return null;
			}
			Node node = (Node) nodes.get(uid);
			if (node == null) {
				node = new Node(registry, uid);
				nodes.put(uid, node);
			}
			return node;
		}

		public Node addNode(final Object id) {
			final String uid = registry.nodeUniqueId(id);
			Node node = (Node) nodes.get(uid);
			if (node == null) {
				node = new Node(registry, uid);
				nodes.put(uid, node);
			}
			return node;
		}

		public Cluster addCluster(final Object id) {
			final String uid = registry.nodeUniqueId(id);
			Cluster node = (Cluster) nodes.get(uid);
			if (node == null) {
				node = new Cluster(registry, uid);
				nodes.put(uid, node);
			}
			return node;
		}

		public AbstractNode addStereotype(final String stereotype) {
			stereotypes.add(DotUmlHelper.stereotype(stereotype));
			return this;
		}

		public Association addExistingAssociation(final Object sourceId,
				final Object targetId) {
			final String uid = registry.existingUniqueId(sourceId);
			final String uid2 = registry.existingUniqueId(targetId);
			if (uid != null && uid2 != null) {
				final Association association = new Association(uid, uid2);
				associations.add(association);
				return association;
			}
			return null;
		}

		public Association addAssociation(final Object sourceId,
				final Object targetId) {
			final String uid = registry.nodeUniqueId(sourceId);
			final String uid2 = registry.nodeUniqueId(targetId);
			if (uid != null && uid2 != null) {
				final Association association = new Association(uid, uid2);
				associations.add(association);
				return association;
			}
			return null;
		}

		public String getLabel() {
			return label;
		}

		public AbstractNode setLabel(final String label) {
			this.label = label;
			return this;
		}

		public String getOptions() {
			return options;
		}

		public AbstractNode setOptions(final String options) {
			this.options = options;
			return this;
		}

		public Collection getAssociations() {
			return associations;
		}

		public String getId() {
			return id;
		}

		public Collection getStereotypes() {
			return stereotypes;
		}

		protected void renderAssociations(final StringBuffer out) {
			final Iterator it = associations.iterator();
			while (it.hasNext()) {
				final Renderable renderable = (Renderable) it.next();
				out.append(renderable.render());
			}
		}

		protected void renderNodes(final StringBuffer out) {
			final List<Renderable> values = new ArrayList<Renderable>(
					nodes.values());
			final Comparator comp = new Comparator<Renderable>() {

				@Override
				public int compare(final Renderable r1, final Renderable r2) {
					return r1.toString().compareTo(r2.toString());
				}
			};
			Collections.sort(values, comp);
			final Iterator it = values.iterator();
			while (it.hasNext()) {
				final Renderable renderable = (Renderable) it.next();
				out.append(renderable.render());
			}
		}

		@Override
		public String toString() {
			return "Node";
		}

		/**
		 * @return true if this Node is equal to the given Node
		 */
		@Override
		public boolean equals(final Object arg0) {
			if (!(arg0 instanceof AbstractNode)) {
				return false;
			}
			final AbstractNode other = (AbstractNode) arg0;
			if (this == other) {
				return true;
			}
			return other.id.equals(id);
		}

		@Override
		public int hashCode() {
			return id.hashCode();
		}

	}

	/**
	 * Represents an actual node (dot node element)
	 * 
	 * @author cyrille martraire
	 */
	public static final class Node extends AbstractNode {

		public Node(final NodeRegistry registry, final String id) {
			super(registry, id);
		}

		@Override
		public String render() {
			if (label == null) {
				return "";
			}
			final StringBuffer out = new StringBuffer();
			if (comment != null) {
				out.append("\n//");
				out.append(comment);
			}

			final List cells = new ArrayList();
			cells.add(label);
			if (!stereotypes.isEmpty()) {
				cells.addAll(stereotypes);
			}
			final String content = DotUmlHelper.line(
					(String[]) cells.toArray(new String[cells.size()]), "\\n ",
					"");

			final String wrapText = DotUmlHelper.wrapText(content, 20);
			out.append(DotUmlHelper.node(id, wrapText, options));

			final Iterator it = associations.iterator();
			while (it.hasNext()) {
				final AbstractAssociation abstractAssociation = (AbstractAssociation) it
						.next();

				out.append(abstractAssociation.render());
			}
			return out.toString();
		}

		@Override
		public String toString() {
			return "Node" + id;
		}
	}

	/**
	 * Represents a dot Digraph element
	 * 
	 * @author cyrille martraire
	 */
	public static final class Digraph extends AbstractNode {

		public Digraph(final NodeRegistry registry, final String title) {
			super(registry, title);
			setLabel(title);
		}

		@Override
		public String render() {
			final StringBuffer out = new StringBuffer();
			out.append(DotUmlHelper.openGraph(label));

			renderNodes(out);
			renderAssociations(out);

			out.append(DotUmlHelper.closeGraph());
			return out.toString();
		}

		@Override
		public String toString() {
			return "Digraph " + id;
		}

		public AbstractNode findNode(final String identifier) {
			final String uid = registry.nodeUniqueId(id);
			final Iterator it = nodes.values().iterator();
			while (it.hasNext()) {
				final AbstractNode node = (AbstractNode) it.next();
				if (uid.equals(node.getId())) {
					return node;
				}
			}

			return null;
		}
	}

	/**
	 * Represents a Node in a dot diagram
	 * 
	 * @author cyrille martraire
	 */
	public static final class Cluster extends AbstractNode {

		public Cluster(final NodeRegistry registry, final String id) {
			super(registry, id);
		}

		@Override
		public String render() {
			final StringBuffer out = new StringBuffer();
			final List cells = new ArrayList();
			if (label != null) {
				cells.add(label);
			}
			cells.addAll(stereotypes);
			final String content = DotUmlHelper.line(
					(String[]) cells.toArray(new String[cells.size()]), "\\n ",
					"");
			out.append(openCluster(id, content, null));

			renderNodes(out);
			renderAssociations(out);

			out.append(closeCluster());
			return out.toString();
		}

		private static String openCluster(final String name,
				final String label, final String options) {
			final StringBuffer sb = new StringBuffer();
			sb.append(CLUSTER_PREFIX + name);
			sb.append(" {");
			sb.append("\nlabel = \"" + label + "\";");

			return sb.toString();
		}

		private static String closeCluster() {
			return "\n}";
		}

		@Override
		public String toString() {
			return "Cluster " + id;
		}

	}

	/**
	 * Represents any association in a dot diagram
	 * 
	 * @author cyrille martraire
	 */
	public static abstract class AbstractAssociation implements Renderable {
		protected final String sourceId;

		protected final String targetId;

		protected String label;

		protected String comment;

		protected String options;

		public AbstractAssociation(final String sourceId, final String targetId) {
			this.sourceId = sourceId;
			this.targetId = targetId;
		}

		public String getLabel() {
			return label;
		}

		public AbstractAssociation setLabel(final String label) {
			if (label != null && label.length() > 0) {
				this.label = label;
			}
			return this;
		}

		public String getComment() {
			return comment;
		}

		public AbstractAssociation setComment(final String comment) {
			this.comment = comment;
			return this;
		}

		public String getOptions() {
			return options;
		}

		public AbstractAssociation setOptions(final String options) {
			this.options = options;
			return this;
		}

		public String getTargetId() {
			return targetId;
		}

		/**
		 * @return true if this Association is equal to the given Association
		 */
		@Override
		public boolean equals(final Object arg0) {
			if (!(arg0 instanceof AbstractAssociation)) {
				return false;
			}
			final AbstractAssociation other = (AbstractAssociation) arg0;
			if (this == other) {
				return true;
			}
			return other.sourceId.equals(sourceId)
					&& other.targetId.equals(targetId);
		}

		@Override
		public int hashCode() {
			return sourceId.hashCode() ^ targetId.hashCode();
		}

	}

	/**
	 * Represents a real association from a node A to a node B
	 * 
	 * @author cyrille martraire
	 */
	public static class Association extends AbstractAssociation {

		public Association(final String sourceId, final String targetId) {
			super(sourceId, targetId);
		}

		@Override
		public String render() {
			final StringBuffer out = new StringBuffer();
			final String displayLabel = label == null ? null : "label=\""
					+ label + "\"";
			out.append(DotUmlHelper.edge(sourceId, targetId, comment,
					displayLabel, options));
			return out.toString();
		}

		@Override
		public String toString() {
			return "Association from " + sourceId + " to " + targetId;
		}
	}

}
